Skip to content

hack: try removing error normalization#4859

Draft
davidhewitt wants to merge 2 commits into
PyO3:mainfrom
davidhewitt:no-err-normalization
Draft

hack: try removing error normalization#4859
davidhewitt wants to merge 2 commits into
PyO3:mainfrom
davidhewitt:no-err-normalization

Conversation

@davidhewitt

@davidhewitt davidhewitt commented Jan 15, 2025

Copy link
Copy Markdown
Member

This is the most minimal diff I could write which removes the storage of "lazy" errors inside PyErr and instead eagerly creates Python exceptions at the point of PyErr creation.

Related to #4584

Now that I push this diff, I see that it's pretty obvious that what this change mainly leads to is that we need a Python<'py> token to create a PyErr (in this PR I do it just internally with Python::with_gil, but maybe this links back to #4413 and we can do that at the same time).

This is not ready to merge yet, I wanted to push it and see what happens in CI...

@codspeed-hq

codspeed-hq Bot commented Jan 15, 2025

Copy link
Copy Markdown

CodSpeed Performance Report

Merging #4859 will degrade performances by 88.05%

Comparing davidhewitt:no-err-normalization (71e0281) with main (da618c7)

Summary

⚡ 14 improvements
❌ 2 regressions
✅ 82 untouched

⚠️ Please fix the performance issues or acknowledge them on CodSpeed.

Benchmarks breakdown

Benchmark BASE HEAD Change
ordered_dunder_methods 2.6 µs 2.3 µs +13.34%
ordered_richcmp 2.4 µs 2.1 µs +16.32%
err_new_without_gil 1.2 µs 10.4 µs -88.05%
extract_str_extract_fail 1.8 µs 10.4 µs -82.3%
enum_from_pyobject 23.6 µs 18.2 µs +29.83%
not_a_list_via_extract_enum 17.6 µs 12.8 µs +38.15%
list_get_item 3.9 ms 3.3 ms +15.79%
list_get_item_unchecked 3.3 ms 3 ms +12.17%
list_nth 4.3 µs 3.7 µs +14.84%
list_nth_back 4.3 µs 3.8 µs +14.67%
tuple_get_borrowed_item 3.6 ms 3.1 ms +15.71%
tuple_get_borrowed_item_unchecked 3.1 ms 2.7 ms +12.64%
tuple_get_item 3.8 ms 3.3 ms +15.99%
tuple_get_item_unchecked 3.3 ms 2.9 ms +12.28%
tuple_nth 3.9 µs 3.4 µs +15.72%
tuple_nth_back 4 µs 3.4 µs +15.53%

@davidhewitt

Copy link
Copy Markdown
Member Author

Looks like (so far at least) the tests pass ok.

The codspeed report suggests to me that:

  • as above, we probably want to do this change alongside [RFC] split PyErr::new() into multiple methods #4413 so that we make it clear creating a PyErr needs the GIL
  • the simplification to error state seems to have a noticeable impact across a lot of whole codebase, it seems desirable from a performance perspective to complete this

@davidhewitt

Copy link
Copy Markdown
Member Author

The performance regressions seem to come from two cases:

  • Creating PyErr without the GIL now blocks on the GIL internally. I'm not worrried about this because I guess that will be a change to the PyErr API (maybe with deprecations).
  • Cases where we create a PyErr (from a DowncastError, though could be any Rust error) and then immediately throw it away without checking the actual error content. Previously this would have just made a lazy error, but now it has to create a full Python exception for more wasted work.

I guess this also implies that we probably want to support a user-defined error type for FromPyObject, so that users have an option to avoid PyErr in such cases? (cc @Icxolu)

@Icxolu

Icxolu commented Jan 16, 2025

Copy link
Copy Markdown
Member

I guess this also implies that we probably want to support a user-defined error type for FromPyObject, so that users have an option to avoid PyErr in such cases?

That looks desirable to me, yes. I believe I have a branch somewhere that introduces FromPyObject::Error: Into<PyErr> that I can revive.

@ngoldbaum

Copy link
Copy Markdown
Contributor

Awesome that this is a significant net decrease in LOC and hopefully it's rare to create and then discard a PyErr in a non-error code path.

Commenting to subscribe, but please feel free to ping me if you want my help with this.

@davidhewitt

davidhewitt commented Sep 27, 2025

Copy link
Copy Markdown
Member Author

Having rebased this, it's clear that this is still a pretty decent performance win across the codebase in general. With Error type for FromPyObject now implemented, with something for &str extract implementation (maybe #5391) we would not have the performance regression reported there and the only codepaths which might have regressions would be those which exploited PyErr's lazy behaviour.

Encouraging proper Rust errors over Python exceptions seems like good design for most codebases except where the error is always going out to Python anyway (in which case the laziness was purely an overhead).

The only question, IMO, is whether it's worth merging this to remove the behaviour from PyErr, or whether we should be introducing a new error handling mechanism and gracefully deprecate PyErr without disrupting users along the way. Given I don't think this is urgent, I'm satisfied to wait for now.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants